Skip to content

various: deprecate %cpu-usage; add new variables#56

Open
NotAShelf wants to merge 17 commits intomainfrom
notashelf/push-ltukuzkpltnp
Open

various: deprecate %cpu-usage; add new variables#56
NotAShelf wants to merge 17 commits intomainfrom
notashelf/push-ltukuzkpltnp

Conversation

@NotAShelf
Copy link
Owner

@NotAShelf NotAShelf commented Feb 17, 2026

Adds a few new system variables per #45. New variables:

  • `$cpu-scaling-maximum - CPU scaling maximum frequency in MHz
  • %cpu-core-count - Number of CPU cores
  • $load-average-since System load average over a given duration
  • ?lid-closed - Boolean for lid state
  • $hour-of-day - Current hour (0-23) based on local time
  • $battery-cycles - Battery cycle count
  • %battery-health - Battery health percentage

Notes:

  • %cpu-usage is now fully deprecated. Updated example config with best-case examples I can think of. Could be optimized further if we decide to actually benchmark power consumption on a live system.

  • Like we do for CPU usage, it's worth adding support for measuring load average over certain durations rather than just fixed 1m, 5m and 15m averages.

    • A system with many I/O-bound processes can show high load average but low CPU usage. Conversely, a few CPU-intensive tasks can show high CPU usage but moderate load. In this case we have a good reason to distinguish the two.
  • $cpu-scaling-minimum is not added, because minimum should be always 0. Maximum is what we'll want to use to infer system "capability". I cannot think of a single case where it will not be 0.

  • ?$hour-of-day is added despite my belief that it'll be obsoleted by the D-Bus interface. It's a trivial addition, and I'd rather not curse people with D-Bus for something so trivial. It's good for some basic rules, and of trivial cost.

  • I'm not particularly proud of the battery aggregation. This'll need to be polished to support per-battery supports.

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Id8593744f5c1b4a862ff9d516570a1986a6a6964
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ie65f50fae6ca27a115b12c3d04c7032f6a6a6964
…bles

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Idf55162fa903329d659caac19bbe92e86a6a6964
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I3d5a06963de7b1f485861ce77607d2226a6a6964
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Id94264310cd7f9ccde4a7d1edd162e336a6a6964
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ia2809ab13363fa591132631f3956579a6a6a6964
@NotAShelf NotAShelf force-pushed the notashelf/push-ltukuzkpltnp branch from 7209726 to 0fea2d2 Compare February 17, 2026 09:04
@NotAShelf NotAShelf changed the title various: deprecatae %cpu-usage; add new variables various: deprecate %cpu-usage; add new variables Feb 17, 2026
Adds `%cpu-core-count`, `?lid-closed`, `$battery-cycles` and `%battery-health` variables.

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I513545b3c54aa7d725423896a295532a6a6a6964
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ic792e0ccffd71f3229c03f91572c72006a6a6964
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ifd02e5852dcfff8720ba3f631c47d00d6a6a6964
Comment on lines +606 to +615
// SW_LID is bit 0 in the capabilities bitmask
// The state file shows the current state of switches as a hex bitmask
// If bit 0 is set, the lid is closed
if let Ok(caps) = u64::from_str_radix(sw_caps.trim(), 16) {
self.lid_closed = (caps & 0x1) != 0;
log::debug!(
"lid state from input device {path}: {state}",
path = entry_path.display(),
state = if self.lid_closed { "closed" } else { "open" }
);
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know how this holds up against different systems. Will need to investigate.

Comment on lines +263 to +287
// Battery health as a percentage (0-100)
// Some systems report this as capacity_level or health
self.health = if let Some(health) =
fs::read_n::<u64>(self.path.join("health"))
.with_context(|| format!("failed to read {self} health"))?
{
Some(health as f64 / 100.0)
} else {
// Try to calculate health from energy_full vs energy_full_design
let energy_full = fs::read_n::<u64>(self.path.join("energy_full"))
.with_context(|| format!("failed to read {self} energy_full"))?;

let energy_full_design =
fs::read_n::<u64>(self.path.join("energy_full_design"))
.with_context(|| {
format!("failed to read {self} energy_full_design")
})?;

match (energy_full, energy_full_design) {
(Some(full), Some(design)) if design > 0 => {
Some(full as f64 / design as f64)
},
_ => None,
}
};
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like we do for paths, we might want to classify those paths depending on the vendor. It's really difficult to support various vendors without diving into the kernel code, which is very time consuming. Though, I don't like assuming things either.

@NotAShelf NotAShelf requested a review from RGBCube February 17, 2026 10:14
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ie8f136a922eebace444a8b40cebd7eac6a6a6964
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: If2d8e55a400a7a52903752fb480728056a6a6964
Now computed on-demand from `state.cpus`` during expression evaluation
rather than stored as pre-computed fields in `EvalState`.

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ifeb78acbb2f8c28468b6bcc912f88b556a6a6964
I guess this was bound to happen and now I feel stupid for wasting my
time with the static expressions earlier. This commit adds
`load-average-since` that computers average load from the CPU log over a
configurable duration, similiar to `cpu-usage-since`.

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I95a9e5dca7a34da5395ade1d6deb25a76a6a6964
I've changed cycles to cycle paths via search & replace previously so it
accidentally regressed the path. This fixes the incorrect sysfs path
from "cycles" to "cycle_count" for reading battery cycle count on Linux
systems.

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ia801fe84abb87cbfbf5e14be14eeab566a6a6964
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I99b2146dfff44569fd90436a836692476a6a6964
@NotAShelf NotAShelf marked this pull request as ready for review February 17, 2026 13:53
@NotAShelf NotAShelf requested a review from RGBCube February 17, 2026 13:53
Adds new expression variants:

- `BatteryCyclesFor { name }` - cycle count for specific battery
- `BatteryHealthFor { name }` - health for specific battery  
- `IsBatteryAvailable { value }` - predicate to check battery existence

Those eplace the aggregated-only `battery_cycles` and `battery_health`.

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I736448f3e8a73a6a90538cd99af188996a6a6964
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ibaeb1235a15aebe0450f6f056c6a9c966a6a6964
*value == T::default()
}

fn find_battery<'a>(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should really be inlined, or defined inside the fn that uses it

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can be moved to eval but I reckon we'll need it elsewhere if we end up adding more battery-specific expressions. For now I'm moving it but I strongly suspect it'll be moved out eventually.

pub frequency_available: bool,
pub turbo_available: bool,

pub cpu_usage: f64,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this unused field

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops.

let max = state
.cpus
.iter()
.filter_map(|cpu| cpu.frequency_mhz_maximum)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure this is the hardware limit. We have no field for software, add cpu.frequency_mhz_software_cap


CpuCoreCount => Number(state.cpus.len() as f64),

LoadAverageSince { duration } => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

duration should be an Expr so people can do ifs etc inside. We have .try_into_string as well.

Comment on lines +240 to +244
let batteries: Vec<_> = self
.power_supplies
.iter()
.filter(|ps| ps.type_ == "Battery" && !ps.is_from_peripheral)
.collect();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't this use the find_batteries? Also, you don't seem to be checking is_from_peripheral in that fn def, I wonder if the disparity is a good idea

Comment on lines +566 to +572
let entry = match entry {
Ok(entry) => entry,
Err(error) => {
log::debug!("failed to read input device entry: {error}");
continue;
},
};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be a let Ok(...) = ... else { log; continue; };

};

// Look for lid switch input device
if name.trim() == "Lid Switch" {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let "Lid Switch" = name.trim() else { log; continue; };
if body here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants